import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  ValidationError,
} from '@nestjs/common';
import iterate from 'iterare';
import { Response } from 'express';
import {
  I18nContext,
  I18nValidationError,
  I18nValidationException,
  I18nValidationExceptionFilterDetailedErrorsOption,
  I18nValidationExceptionFilterErrorFormatterOption,
} from 'nestjs-i18n';
import {
  formatI18nErrors,
  mapChildrenToValidationErrors,
} from 'nestjs-i18n/dist/utils';
import { ResponseUtil } from '../utils/response.util';

type I18nValidationExceptionFilterOptions =
  | I18nValidationExceptionFilterDetailedErrorsOption
  | I18nValidationExceptionFilterErrorFormatterOption;

@Catch(I18nValidationException)
export class I18nValidationExceptionFilter implements ExceptionFilter {
  constructor(
    private readonly options: I18nValidationExceptionFilterOptions = {
      detailedErrors: true,
    },
  ) {}
  catch(exception: I18nValidationException, host: ArgumentsHost) {
    const i18n = I18nContext.current();

    const errors = formatI18nErrors(
      this.joinStr(exception.errors),
      i18n.service,
      {
        lang: i18n.lang,
      },
    );
    const normalizedErrors = this.normalizeValidationErrors(errors);
    switch (host.getType() as string) {
      case 'http':
        const response = host.switchToHttp().getResponse<Response>();
        response.json(
          ResponseUtil.fail(
            this.options.errorHttpStatusCode ?? exception.getStatus(),
            this.formatErrors(normalizedErrors),
          ),
        );
        break;
      case 'graphql':
        exception.errors = normalizedErrors as I18nValidationError[];
        return exception;
    }
  }

  private joinStr(errors: ValidationError[]) {
    return errors.length
      ? errors.map((error) => {
          Object.entries(error.constraints).forEach(([key, value]) => {
            // 给值加上前缀 "index."
            error.constraints[key] = `${ResponseUtil.index}.${value}`;
          });
          return error;
        })
      : [];
  }

  private formatErrors(errors: object | ValidationError[] | string[]) {
    if (Array.isArray(errors)) {
      const prefix = `${ResponseUtil.index}.`;
      return errors
        .map((value: any) => {
          if (typeof value === 'string' && value.startsWith(prefix)) {
            return value.substring(prefix.length);
          }
          return value;
        })
        .join(',');
    } else {
      return JSON.stringify(errors);
    }
  }

  private isWithErrorFormatter(
    options: I18nValidationExceptionFilterOptions,
  ): options is I18nValidationExceptionFilterErrorFormatterOption {
    return 'errorFormatter' in options;
  }

  protected normalizeValidationErrors(
    validationErrors: ValidationError[],
  ): string[] | I18nValidationError[] | object {
    if (
      this.isWithErrorFormatter(this.options) &&
      !('detailedErrors' in this.options)
    ) {
      return this.options.errorFormatter(validationErrors);
    }

    if (
      !this.isWithErrorFormatter(this.options) &&
      !this.options.detailedErrors
    ) {
      return this.flattenValidationErrors(validationErrors);
    }

    return validationErrors;
  }

  protected flattenValidationErrors(
    validationErrors: ValidationError[],
  ): string[] {
    return iterate(validationErrors)
      .map((error) => mapChildrenToValidationErrors(error))
      .flatten()
      .filter((item) => !!item.constraints)
      .map((item) => Object.values(item.constraints))
      .flatten()
      .toArray();
  }
}
